package Euler54;

import java.util.*;
import Euler54.Card.*;

public class Hand implements Comparable<Hand> {
	public enum Rank
	{
		HighCard,
		OnePair,
		TwoPairs,
		ThreeOfAKind,
		Straight,
		Flush,
		FullHouse,
		FourOfAKind,
		StraightFlush,
		RoyalFlush;
	}
	
	private List<Card> cards;
	
	public Hand()
	{
		cards = new ArrayList<Card>();
	}
	
	public Hand(String cards)
	{
		this.cards = new ArrayList<Card>();
		String tokens[] = cards.split(" ");
		addCards(tokens);
	}
	
	public Hand(String cards[])
	{
		this.cards = new ArrayList<Card>();
		addCards(cards);
	}
	
	//Add a card, should be optimized.
	public void addCard(Card card)
	{	
		for(int i = 0; i < cards.size(); i++)
		{
			Card curr = cards.get(i);
			
			if(curr.compareTo(card)>=0)
			{
				cards.add(i, card);
				return;
			}
		}
		
		cards.add(card);
	}
	
	public Card getCard(int index)
	{
		return cards.get(index);
	}
	
	public void removeCard(int index)
	{
		cards.remove(index);
	}
	
	public List<Card> getCards()
	{
		return cards;
	}
	
	public Iterator<Card> iterator()
	{
		return cards.iterator();
	}
	
	public int numCards()
	{
		return cards.size();
	}
	
	private void addCards(String cards[])
	{
		for(int i = 0; i < cards.length; i++)
		{
			addCard(new Card(cards[i]));
		}
	}
	
	public Rank getHandRank()
	{
		boolean isFlush = false, isStraight = false;
		HashSet<SUIT> suit = new HashSet<SUIT>();
		HashMap<RANK, Integer> ranks = new HashMap<RANK, Integer>();
	
		if(cards.size() != 5)
		{
			return Rank.HighCard;
		}
		
		for(Iterator<Card> iter = cards.iterator(); iter.hasNext();)
		{
			//Get the current card.
			Card next = iter.next();
			
			//Add the suit to the suit hash.
			suit.add(next.getSuite());
			
			//Increment the card rank hash counter.
			if(ranks.containsKey(next.getRank()))
			{
				ranks.put(next.getRank(), ranks.get(next.getRank())+1);
			}
			else
			{
				ranks.put(next.getRank(), 1);
			}
		}
		
		//Check if the hand is a flush.
		if(suit.size()==1)
		{
			isFlush=true;
		}
		
		//Check if the hand is a straight.
		if(ranks.size()==cards.size())
		{
			Card prev = null;
			for(Iterator<Card> iter = cards.iterator(); iter.hasNext();)
			{
				//Get the current card.
				Card next = iter.next();
				
				if(prev==null)
				{
					prev = next;
				}
				else
				{
					if(next.compareTo(prev) != 1)
					{
						break;
					}
					else if (!iter.hasNext())
					{
						isStraight=true;
					}
					prev = next;
				}
			}
		}
		
		//In the event of a straight, the highest hand must be a straight, straight flush, or royal flush.
		if(isStraight)
		{
			if(isFlush)
			{
				if(cards.get(0).getRank() == RANK.TEN)
				{
					return Rank.RoyalFlush;
				}
				else
				{
					return Rank.StraightFlush;
				}
			}
			else
			{
				return Rank.Straight;
			}
		}
		else
		{
			//Full house or four of a kind.
			if(ranks.size()==2)
			{
				  Iterator<Integer> iter = ranks.values().iterator();
				  int val = 0;
				  if(iter.hasNext())
				  {
					  val = iter.next();
				  }
				  
				  if(val==1||val==4)
				  {
					  return Rank.FourOfAKind;
				  }
				  else
				  {
					  return Rank.FullHouse;
				  }
			}
			//A flush.
			else if(isFlush)
			{
				return Rank.Flush;
			}
			//Two pair or three of a kind.
			else if(ranks.size()==3)
			{
				for(Iterator<Integer> iter = ranks.values().iterator(); iter.hasNext();)
				{
					int val = iter.next();
					if(val==2)
					{
						return Rank.TwoPairs;
					}
					else if(val==3)
					{
						return Rank.ThreeOfAKind;
					}
				}
			}
			//One pair.
			else if(ranks.size()==4)
			{
				return Rank.OnePair;
			}
		}
		
		//High card. (default)
		return Rank.HighCard;
	}
	
	public int compareHighCard(Hand other)
	{
		int comp = 0;
		if(this.numCards()==other.numCards() && this.numCards()>0){
			for(int i = (this.numCards()-1); i >= 0; i--)
			{
				int cmp = this.getCard(i).getRank().compareTo(other.getCard(i).getRank());
				if(cmp!=0)
				{
					if(cmp>0)
					{
						comp = 1;
					}
					else if(cmp<0)
					{
						comp = -1;
					}
					break;
				}
			}
		}
		
		return comp;
	}
	
	@Override
	public int compareTo(Hand other) 
	{
		Rank a = this.getHandRank();
		Rank b = other.getHandRank();
		int comp = a.compareTo(b);
		
		if(comp==0)
		{
			HashMap<RANK, Integer> ranksA = new HashMap<RANK, Integer>();
			HashMap<RANK, Integer> ranksB = new HashMap<RANK, Integer>();
			RANK setA = RANK.TWO, setB = RANK.TWO;
			int temp=0;
			
			switch(a)
			{
			case HighCard:
			case Flush:
			case Straight:
			case StraightFlush:
				comp = this.compareHighCard(other);
				break;
			case RoyalFlush:
				comp = 0;
				break;
			case OnePair:
				for(int i = 0; i < cards.size(); i++)
				{
					//Get the current card.
					Card nextA = this.getCard(i);
					Card nextB = other.getCard(i);
					
					//Increment the card rank hash counter.
					if(ranksB.containsKey(nextB.getRank()))
					{
						ranksB.put(nextB.getRank(), ranksB.get(nextB.getRank())+1);
						setB = nextB.getRank();
					}
					else
					{
						ranksB.put(nextB.getRank(), 1);
					}
					
					//Increment the card rank hash counter.
					if(ranksA.containsKey(nextA.getRank()))
					{
						ranksA.put(nextA.getRank(), ranksA.get(nextA.getRank())+1);
						setA = nextA.getRank();
					}
					else
					{
						ranksA.put(nextA.getRank(), 1);
					}
				}
				
				temp = setA.compareTo(setB);
				if(temp==0)
				{
					comp = this.compareHighCard(other);
				}
				else
				{
					comp = temp;
				}
				
				break;
			case TwoPairs:
				for(int i = 0; i < cards.size(); i++)
				{
					//Get the current card.
					Card nextA = this.getCard(i);
					Card nextB = other.getCard(i);
					
					//Increment the card rank hash counter.
					if(ranksB.containsKey(nextB.getRank()))
					{
						ranksB.put(nextB.getRank(), ranksB.get(nextB.getRank())+1);
						if(setB.compareTo(nextB.getRank())<0)
						{
							setB = nextB.getRank();
						}
					}
					else
					{
						ranksB.put(nextB.getRank(), 1);
					}
					
					//Increment the card rank hash counter.
					if(ranksA.containsKey(nextA.getRank()))
					{
						ranksA.put(nextA.getRank(), ranksA.get(nextA.getRank())+1);
						if(setA.compareTo(nextA.getRank())<0)
						{
							setA = nextA.getRank();
						}
					}
					else
					{
						ranksA.put(nextA.getRank(), 1);
					}
				}
				
				temp = setA.compareTo(setB);
				if(temp==0)
				{
					comp = this.compareHighCard(other);
				}
				else
				{
					comp = temp;
				}
				break;
			case ThreeOfAKind:
			case FullHouse:
			case FourOfAKind:
				for(int i = 0; i < cards.size(); i++)
				{
					//Get the current card.
					Card nextA = this.getCard(i);
					Card nextB = other.getCard(i);
					
					//Increment the card rank hash counter.
					if(ranksB.containsKey(nextB.getRank()))
					{
						int count = ranksB.get(nextB.getRank());
						ranksB.put(nextB.getRank(), count+1);
						
						if(count>=2)
						{
							setB = nextB.getRank();
						}
						
					}
					else
					{
						ranksB.put(nextB.getRank(), 1);
					}
					
					//Increment the card rank hash counter.
					if(ranksA.containsKey(nextA.getRank()))
					{
						int count =  ranksA.get(nextA.getRank());
						ranksA.put(nextA.getRank(),count+1);
						
						if(count>=2)
						{
							setA = nextA.getRank();
						}
					}
					else
					{
						ranksA.put(nextA.getRank(), 1);
					}
				}
				
				temp = setA.compareTo(setB);
				if(temp==0)
				{
					comp = this.compareHighCard(other);
				}
				else
				{
					comp = temp;
				}
				break;
			}
		}
		
		return comp;
	}
	
	@Override
	public String toString() 
	{
		String temp = "";
		
		for(Iterator<Card> iter = cards.iterator(); iter.hasNext();)
		{
			Card next = iter.next();
			
			if(iter.hasNext())
			{
				temp += next.toString() + ", ";
			}
			else
			{
				temp += next.toString() + ".";
			}
		}
		
		return temp;
	}
}